/*
 * Copyright (C) 2012  Romain DUBOIS
 * 
 * This file is part of NeMoS - Network and Modular Software
 * 
 * NeMoS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * NeMoS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with NeMoS.  If not, see <http://www.gnu.org/licenses/>.
 */
package nemos.resources.impl.internal;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.ResourceBundle.Control;
import java.util.concurrent.atomic.AtomicReference;

import nemos.resources.IResources;
import nemos.resources.IResourcesManager;
import nemos.resources.IResourcesListener;
import nemos.resources.UnsupportedResourceType;
import nemos.resources.impl.IControl;
import nemos.resources.impl.IExternalFolder;
import nemos.util.PropertyHelper;
import nemos.util.i18n.BundleLocalization;
import nemos.util.osgi.BundleClassLoader;
import nemos.util.osgi.FilterBuilder;

import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.ConfigurationPolicy;
import org.apache.felix.scr.annotations.Modified;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.ReferenceStrategy;
import org.apache.felix.scr.annotations.Service;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.log.LogService;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;
import org.osgi.service.prefs.PreferencesService;

/**
 * ResourcesManagerImpl : The {@link IResourcesManager} standard implementation.
 * 
 * <p>
 * This implementation delagates the {@link IResources} generation to the
 * {@link IControl} services and the external File resolution to the
 * {@link IExternalFolder} services.
 * </p>
 * 
 * @see IControl
 * @see IExternalFolder
 * 
 * @author Romain DUBOIS
 */
@Reference(name = "folder", referenceInterface = IExternalFolder.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, policy = ReferencePolicy.DYNAMIC)
@Service(IResourcesManager.class)
@Component(specVersion = "1.1", name = "nemos.resources", policy = ConfigurationPolicy.OPTIONAL)
public class ResourcesManagerImpl extends AbstractFileManager implements IResourcesManager {

    /** The preferences service reference name */
    private static final String PREFERENCES_REF = "preferences"; //$NON-NLS-1$

    /** Java property : system user */
    private static final String USER_NAME = "user.name"; //$NON-NLS-1$

    /** The user locale language property name */
    private static final String LOCALE_LANGUAGE = "locale.lang"; //$NON-NLS-1$
    /** The user locale country property name */
    private static final String LOCALE_COUNTRY = "locale.country"; //$NON-NLS-1$
    /** The user locale variant property name */
    private static final String LOCALE_VARIANT = "locale.variant"; //$NON-NLS-1$
    /** The locale fields */
    private static final String[] LOCALE_FIELDS = new String[] { LOCALE_LANGUAGE, LOCALE_COUNTRY,
            LOCALE_VARIANT };
    /** The launch locale property name */
    private static final String LAUNCH_LOCALE_PROPERTY = "nemos.launch.locale"; //$NON-NLS-1$
    /** The default user base name property name */
    private static final String USER_DEFAULT_BASENAME = "nemos.resources.ui"; //$NON-NLS-1$

    /** The fallback dir property */
    private static final String FALLBACK_DIR_PROPERTY = "nemos.resources.fallbackDir"; //$NON-NLS-1$
    /** The default fallback dir */
    private static final String DEFAULT_FALLBACK_DIR = "resources-default"; //$NON-NLS-1$

    /** The component context */
    private ComponentContext mContext;

    /** The Folder services map */
    private final Map<String, IExternalFolder> mFolders;
    /** The default folder used for types with no service */
    private File mDefaultFolder;

    /** The log service */
    @Reference(name = "log", referenceInterface = LogService.class, cardinality = ReferenceCardinality.OPTIONAL_UNARY)
    private final AtomicReference<LogService> mLogger;
    /** The bundle localization */
    private BundleLocalization mLocalization;

    /** The user preferences */
    @Reference(name = PREFERENCES_REF, referenceInterface = PreferencesService.class, cardinality = ReferenceCardinality.OPTIONAL_UNARY, strategy = ReferenceStrategy.LOOKUP)
    private Preferences mPreferences;
    /** The user locale listeners */
    @Reference(name = "listener", referenceInterface = IResourcesListener.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, policy = ReferencePolicy.DYNAMIC)
    private final Collection<IResourcesListener> mListeners;
    /** The current user locale */
    private Locale mUserLocale;
    /** The default user base name */
    private String mDefaultBaseName;

    /**
     * Constructor
     */
    public ResourcesManagerImpl() {
        super();
        mFolders = new HashMap<String, IExternalFolder>();
        mDefaultFolder = new File(DEFAULT_FALLBACK_DIR);
        mListeners = Collections.synchronizedCollection(new ArrayList<IResourcesListener>());
        mLogger = new AtomicReference<LogService>();
    }

    /**
     * {@inheritDoc}
     * 
     * @see nemos.resources.IResourcesManager#getExternalFile(java.lang.String,
     *      java.lang.String)
     */
    @Override
    public File getExternalFile(final String pType, final String pName) {
        final IExternalFolder lFolder = mFolders.get(pType);

        File lFile;
        if (lFolder == null) {
            final File lDir = new File(mDefaultFolder, escape(pType));
            if (!lDir.exists()) {
                lDir.mkdirs();
            }
            lFile = new File(lDir, escape(pName));
        } else {
            lFile = lFolder.getExternalFile(pName);
        }

        return lFile;
    }

    /**
     * {@inheritDoc}
     * 
     * @see nemos.resources.IResourcesManager#getResources(java.lang.String,
     *      java.lang.Class)
     */
    @Override
    public <T> IResources<T> getResources(final String pBaseName, final Class<T> pType)
            throws UnsupportedResourceType {
        return getResources(pBaseName, pType, null);
    }

    /**
     * {@inheritDoc}
     * 
     * @see nemos.resources.IResourcesManager#getResources(java.lang.String,
     *      java.lang.Class, java.util.Locale)
     */
    @Override
    public <T> IResources<T> getResources(final String pBaseName, final Class<T> pType,
            final Locale pLocale) throws UnsupportedResourceType {
        final BundleContext lContext = mContext.getBundleContext();

        // Check parameters
        if (pType == null) {
            throw new UnsupportedResourceType(pType);
        }
        Locale lLocale = pLocale;
        if (lLocale == null) {
            lLocale = Locale.getDefault();
        }

        // Look for control
        IControl lControlSvc = null;
        ServiceReference<IControl> lRef = null;
        try {
            final String lFilter = FilterBuilder.getStringFilter(IControl.TYPE_PROPERTY,
                    pType.getName());
            final Iterator<ServiceReference<IControl>> lRefsIt = lContext.getServiceReferences(
                    IControl.class, lFilter).iterator();

            while (lControlSvc == null && lRefsIt.hasNext()) {
                lRef = lRefsIt.next();
                lControlSvc = lContext.getService(lRef);
            }
        } catch (final InvalidSyntaxException e) {

            // Local copy in order to avoid synchronized bloc
            final LogService lLogger = mLogger.get();

            if (lLogger != null) {
                lLogger.log(LogService.LOG_ERROR, "Internal error", e); //$NON-NLS-1$
            }
        }

        try {
            // Build the real Control object
            Control lControl = null;
            if (lControlSvc == null) {
                lRef = null;

                // Try the default types
                if (pType == String.class) {
                    lControl = Control.getControl(Control.FORMAT_PROPERTIES);
                } else if (pType == Object.class) {
                    lControl = Control.getControl(Control.FORMAT_CLASS);
                } else {
                    throw new UnsupportedResourceType(pType);
                }
            }
            // Adapt the service
            else {
                lControl = new ControlAdapter(lControlSvc, pType);
            }

            // Rely on the java ResourceBundle mechanism
            try {
                final ResourceBundle lRsrcBundle = ResourceBundle.getBundle(pBaseName, lLocale,
                        new BundleClassLoader(lContext.getBundle()), lControl);

                // Adapt the result
                IResources<T> lResult = null;
                if (lRsrcBundle instanceof IResources) {
                    lResult = ((IResources<?>) lRsrcBundle).adapt(pType);
                } else if (lRsrcBundle != null) {
                    lResult = new ResourcesAdapter<T>(lRsrcBundle, pType);
                }
                return lResult;

            } catch (final MissingResourceException e) {
                return null;
            }
        } finally {
            if (lControlSvc != null) {
                lContext.ungetService(lRef);
            }
        }
    }

    /**
     * Start the component.
     * 
     * @param pContext
     *            The component context
     */
    @Activate
    protected void start(final ComponentContext pContext) {
        mContext = pContext;

        mLocalization = new BundleLocalization(ResourcesManagerImpl.class);

        final PreferencesService lPrefs = (PreferencesService) mContext
                .locateService(PREFERENCES_REF);

        // Preferences is an optional reference
        if (lPrefs != null) {
            mPreferences = lPrefs.getUserPreferences(System.getProperty(USER_NAME));

            // Find the locale
            final String lLang = mPreferences.get(LOCALE_LANGUAGE, null);
            final String lCountry = mPreferences.get(LOCALE_COUNTRY, null);
            final String lVariant = mPreferences.get(LOCALE_VARIANT, null);
            Locale lNewLocale;
            if (lLang == null) {
                lNewLocale = null;
            } else if (lCountry == null) {
                lNewLocale = new Locale(lLang);
            } else if (lVariant == null) {
                lNewLocale = new Locale(lLang, lCountry);
            } else {
                lNewLocale = new Locale(lLang, lCountry, lVariant);
            }

            // Change the locale (and notify)
            if (lNewLocale != null) {
                setUserLocale(lNewLocale);
            }
        }

        // Try the external property
        if (mUserLocale == null) {
            final String lLocaleStr = pContext.getBundleContext().getProperty(
                    LAUNCH_LOCALE_PROPERTY);
            Locale lLocale = Locale.getDefault();
            if (lLocaleStr != null) {
                final String[] lParts = lLocaleStr.split(String.valueOf('_'));
                if (lParts.length > 2) {
                    lLocale = new Locale(lParts[0], lParts[1], lParts[2]);
                } else if (lParts.length == 2) {
                    lLocale = new Locale(lParts[0], lParts[1]);
                } else {
                    lLocale = new Locale(lParts[0]);
                }
            }
            setUserLocale(lLocale);
        }

        // Update properties
        @SuppressWarnings("unchecked")
        final Map<String, Object> lProps = PropertyHelper.copy(pContext.getProperties());
        updated(lProps);
    }

    /**
     * Update the component properties.
     * 
     * @param pProperties
     *            The component properties
     */
    @Modified
    protected void updated(final Map<String, Object> pProperties) {
        mDefaultFolder = checkOrCreateFolder(pProperties, FALLBACK_DIR_PROPERTY,
                DEFAULT_FALLBACK_DIR);
        mDefaultBaseName = (String) pProperties.get(USER_DEFAULT_BASENAME);
    }

    /**
     * Bind a new folder.
     * 
     * @param pFolder
     *            The folder to bind
     * @param pParams
     *            The folder service parameters
     */
    protected void bindFolder(final IExternalFolder pFolder, final Map<String, Object> pParams) {
        final Object lTypes = pParams.get(IExternalFolder.TYPE_PROPERTY);

        // Can be an array
        if (lTypes instanceof String[]) {
            synchronized (mFolders) {
                for (final String lType : (String[]) lTypes) {
                    mFolders.put(lType, pFolder);
                }
            }
        }
        // A collection
        else if (lTypes instanceof Collection<?>) {
            synchronized (mFolders) {
                for (final Object lType : (Collection<?>) lTypes) {
                    if (lType instanceof String) {
                        mFolders.put((String) lType, pFolder);
                    }
                }
            }
        }
        // Or a single one
        else if (lTypes instanceof String) {
            mFolders.put(lTypes.toString(), pFolder);
        }
    }

    /**
     * Unbind a folder.
     * 
     * @param pFolder
     *            The folder to unbind
     */
    protected void unbindFolder(final IExternalFolder pFolder) {
        synchronized (mFolders) {
            final Iterator<Entry<String, IExternalFolder>> lIt = mFolders.entrySet().iterator();
            while (lIt.hasNext()) {
                if (lIt.next().getValue().equals(pFolder)) {
                    lIt.remove();
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see nemos.resources.IResourcesManager#getUserResources()
     */
    @Override
    public String getUserResources() {
        return mDefaultBaseName;
    }

    /**
     * {@inheritDoc}
     * 
     * @see nemos.resources.IResourcesManager#setUserResources(java.lang.String)
     */
    @Override
    public void setUserResources(final String pBaseName) {
        mDefaultBaseName = pBaseName;
    }

    /**
     * {@inheritDoc}
     * 
     * @see nemos.resources.IResourcesManager#getUserLocale()
     */
    @Override
    public Locale getUserLocale() {
        return mUserLocale;
    }

    /**
     * {@inheritDoc}
     * 
     * @see nemos.resources.IResourcesManager#setUserLocale(java.util.Locale)
     */
    @Override
    public void setUserLocale(final Locale pLocale) {
        // Detect real change
        Locale lNewLocale = pLocale;
        if (pLocale == null) {
            lNewLocale = Locale.getDefault();
        }
        if (!lNewLocale.equals(mUserLocale)) {
            mUserLocale = lNewLocale;

            // Local copy to minimize synchronization block
            Collection<IResourcesListener> lListeners;
            synchronized (mListeners) {
                lListeners = new ArrayList<IResourcesListener>(mListeners);
            }

            // Notify listeners
            for (final IResourcesListener lListener : lListeners) {
                lListener.userLocaleChanged(lNewLocale);
            }

            // Try to update preferences
            if (mPreferences == null) {
                final PreferencesService lPrefs = (PreferencesService) mContext
                        .locateService(PREFERENCES_REF);
                if (lPrefs != null) {
                    mPreferences = lPrefs.getUserPreferences(System.getProperty(USER_NAME));
                }
            }
            if (mPreferences != null) {
                // Prepare values to store
                final String[] lValues = new String[3];
                if (!lNewLocale.getLanguage().isEmpty()) {
                    lValues[0] = lNewLocale.getLanguage();
                }
                if (lValues[0] != null && !lNewLocale.getCountry().isEmpty()) {
                    lValues[1] = lNewLocale.getCountry();
                }
                if (lValues[1] != null && !lNewLocale.getVariant().isEmpty()) {
                    lValues[2] = lNewLocale.getVariant();
                }

                // Update preferences
                for (int i = 0; i < LOCALE_FIELDS.length; i++) {
                    if (lValues[i] == null) {
                        mPreferences.remove(LOCALE_FIELDS[i]);
                    } else {
                        mPreferences.put(LOCALE_FIELDS[i], lValues[i]);
                    }
                }
                try {
                    mPreferences.flush();
                } catch (final BackingStoreException e) {
                    final LogService lLogger = mLogger.get();
                    if (lLogger != null) {
                        final String lText = mLocalization.getMessage(
                                ResourcesMessage.LOCALE_UPDATE_FAILED, lNewLocale,
                                mPreferences.name());
                        lLogger.log(LogService.LOG_ERROR, lText, e);
                    }
                }
            }
        }
    }

    /**
     * Bind the Log service.
     * 
     * @param pLogService
     *            The log service
     */
    protected void bindLog(final LogService pLogService) {
        mLogger.set(pLogService);
    }

    /**
     * Unbind the Log service.
     * 
     * @param pLogService
     *            The log service
     */
    protected void unbindLog(final LogService pLogService) {
        mLogger.compareAndSet(pLogService, null);
    }

    /**
     * Bind a listener.
     * 
     * @param pListener
     *            The listener to bind
     */
    public void bindListener(final IResourcesListener pListener) {
        synchronized (mListeners) {
            mListeners.add(pListener);
            pListener.userLocaleChanged(mUserLocale);
        }
    }

    /**
     * Unbind a listener.
     * 
     * @param pListener
     *            The listener to unbind
     */
    public void unbindListener(final IResourcesListener pListener) {
        mListeners.remove(pListener);
    }
}
